﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.Phone.Shell;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using System.Windows.Navigation;
using System.Threading;
using System.Reflection;
using System.Linq;
using Chrysalis.Composition;

namespace Chrysalis.Services
{
    public class ChrysalisService : IApplicationService
    {
        // *** Constants ***

        private const string StateKey_Service = "Chrysalis_Service_{0}";

        // *** Fields ***

        private bool isActivating;
        private Dictionary<Type, object> serviceDictionary = new Dictionary<Type, object>();

        // *** Events ***

        public event EventHandler<ViewModelEventArgs> ViewModelInitialized;

        // *** Properties ***

        public static ChrysalisService Current
        {
            get;
            private set;
        }

        public PhoneApplicationPage CurrentPage
        {
            get;
            private set;
        }

        public object CurrentViewModel
        {
            get
            {
                return CurrentPage == null ? null : CurrentPage.DataContext;
            }
        }

        // *** Private Properties ***

        private PhoneApplicationPage PreviousPage
        {
            get;
            set;
        }

        private object PreviousViewModel
        {
            get
            {
                return PreviousPage == null ? null : PreviousPage.DataContext;
            }
        }

        // *** Methods ***

        public T GetService<T>()
        {
            return (T)GetService(typeof(T));
        }

        public object GetService(Type serviceType)
        {
            // If the service type is an interface, then try to determine the implementation

            if (serviceType.IsInterface)
                return GetServiceFromInterface(serviceType);

            // Try to get the service from the service dictionary if available

            object cachedService;

            if (serviceDictionary.TryGetValue(serviceType, out cachedService))
                return cachedService;

            // Otherwise create a new instance of the service

            object service = Activator.CreateInstance(serviceType);
            serviceDictionary.Add(serviceType, service);

            // Initialize the service

            InitializeService(service, serviceType);

            return service;
        }

        public void SatisfyImports(object target)
        {
            // Get the type of the target object and enuerate all properties

            Type targetType = target.GetType();
            PropertyInfo[] properties = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

            foreach (PropertyInfo property in properties)
            {
                ChrysalisImportAttribute importAttribute = Attribute.GetCustomAttribute(property, typeof(ChrysalisImportAttribute)) as ChrysalisImportAttribute;

                if (importAttribute != null)
                {
                    object value = GetService(property.PropertyType);
                    property.SetValue(target, value, null);
                }
            }
        }

        // *** IApplicationService Methods ***

        void IApplicationService.StartService(ApplicationServiceContext context)
        {
            // Set this as the current ServiceManager via the static property

            ChrysalisService.Current = this;

            // Attach to the PhoneApplicationService lifetime events

            PhoneApplicationService phoneApplicationService = PhoneApplicationService.Current;

            phoneApplicationService.Activated += new EventHandler<ActivatedEventArgs>(PhoneApplicationService_Activated);
            phoneApplicationService.Deactivated += new EventHandler<DeactivatedEventArgs>(PhoneApplicationService_Deactivated);
            phoneApplicationService.Closing += new EventHandler<ClosingEventArgs>(PhoneApplicationService_Closing);

            // Attach to the page navigation events
            // NB: We set off a DispatcherTimer since we need to wait for the RootVisual property to be set before we can access this
            // TODO : Find a better way to do this?

            ExecuteViaDispatcherTimer(() =>
                {
                    if (Application.Current.RootVisual != null)
                        PhoneApplicationFrame_RootVisualLoaded((PhoneApplicationFrame)Application.Current.RootVisual);
                });
        }

        void IApplicationService.StopService()
        {
        }

        // *** Private Methods ***

        private void ExecuteViaDispatcherTimer(Action action)
        {
            // Create a new DispatcherTimer with an interval of zero
            // NB: This will tick when the Dispatcher next gets chance

            DispatcherTimer timer = new DispatcherTimer() { Interval = TimeSpan.FromMilliseconds(0) };

            // When the timer ticks, we call the action and stop the timer so that it only fires once

            timer.Tick += (sender, e) =>
            {
                action();
                ((DispatcherTimer)sender).Stop();
            };

            // Signal the timer to start

            timer.Start();
        }

        private void ExecuteViaDispatcher(Action action)
        {
            // Exectue the action via the current dispatcher

            Deployment.Current.Dispatcher.BeginInvoke(action);
        }

        private void ExecuteViaLayoutUpdated(PhoneApplicationPage page, Action action)
        {
            // Execute the action on the LayoutUpdated event
            // NB : Make sure we unsubscribe from the event so this is only executed once

            EventHandler eventHander = null;

            eventHander = (object sender, EventArgs e) =>
                {
                    action();
                    page.LayoutUpdated -= eventHander;
                };

            page.LayoutUpdated += eventHander;
        }

        private object GetServiceFromInterface(Type interfaceType)
        {
            // If an ExportImplementation attribute is defined then use this to determine the concrete type

            ExportImplementationAttribute exportImplementationAttribute = Attribute.GetCustomAttribute(interfaceType, typeof(ExportImplementationAttribute)) as ExportImplementationAttribute;

            if (exportImplementationAttribute != null)
                return GetService(exportImplementationAttribute.ImplementationType);

            // We have been unsuccessful - return null

            return null;
        }

        private string GetServiceStateKey(Type serviceType)
        {
            // Get a key name to represent a service state

            return string.Format(StateKey_Service, serviceType.FullName);
        }

        private void InitializeService(object service, Type serviceType)
        {
            // If the service implements IHasSessionState - Get stored state for this service if available and initialize

            if (service is IHasSessionState)
            {
                IHasSessionState phoneService = (IHasSessionState)service;

                string serviceStateKey = GetServiceStateKey(serviceType);
                object serviceState = RetrieveApplicationState<object>(serviceStateKey);
                phoneService.Initialize(serviceState);
            }
        }

        private T RetrieveApplicationState<T>(string key)
        {
            // Try to get the state from the dictionary, and check it is the correct type

            object value;

            if (PhoneApplicationService.Current.State.TryGetValue(key, out value))
            {
                if (value is T)
                {
                    // The key has been found and the value is the correct type, so remove it from the dictionary and return

                    PhoneApplicationService.Current.State.Remove(key);
                    return (T)value;
                }
            }

            // Otherwise just return the default value

            return default(T);
        }

        private T RetrievePageState<T>(PhoneApplicationPage page, string key)
        {
            // Try to get the state from the dictionary, and check it is the correct type

            object value;

            if (page.State.TryGetValue(key, out value))
            {
                if (value is T)
                {
                    // The key has been found and the value is the correct type, so remove it from the dictionary and return

                    page.State.Remove(key);
                    return (T)value;
                }
            }

            // Otherwise just return the default value

            return default(T);
        }

        private void SignalNavigatedTo(object viewModel, PhoneApplicationPage page, NavigationEventArgs e)
        {
            // NB: This is done on the dispatcher (therefore after the page's real OnNavigatedTo) so that we can query the state

            ExecuteViaDispatcher(() =>
            {
                // If the view model implements IHasSessionState, then call Initialize (with previous state if available)
                // NB: We use a tag on the page to ensure that the view-model is only initialized once

                if (viewModel is IHasSessionState && ChrysalisPageTag.GetIsInitialized(CurrentPage) == false)
                {
                    object state = RetrievePageState<object>(page, "Chrysalis_State");
                    ((IHasSessionState)viewModel).Initialize(state);
                    ChrysalisPageTag.SetIsInitialized(CurrentPage, true);
                }

                // If the view model implements IPhoneNavigationAware, then call NavigatingTo()

                if (viewModel is IPhoneNavigationAware)
                    ((IPhoneNavigationAware)viewModel).NavigatedTo(e);

                // Finally raise the ViewModelInitialized event

                OnViewModelInitialized(viewModel);
            });
        }

        private void SignalNavigatedFrom(object viewModel, PhoneApplicationPage page, NavigationEventArgs e)
        {
            // If the view model implements IPhoneNavigationAware, then call NavigatedFrom()

            if (viewModel is IPhoneNavigationAware)
                ((IPhoneNavigationAware)viewModel).NavigatedFrom(e);

            // If the view model implements IHasSessionState, then call SaveState and store the result

            if (viewModel is IHasSessionState)
            {
                object state = ((IHasSessionState)viewModel).SaveState();
                page.State["Chrysalis_State"] = state;
            }
        }

        private void SignalNavigatingFrom(object viewModel, PhoneApplicationPage page, NavigatingCancelEventArgs e)
        {
            // If the view model implements IPhoneNavigationAware, then call NavigatingFrom()

            if (viewModel is IPhoneNavigationAware)
                ((IPhoneNavigationAware)viewModel).NavigatingFrom(e);
        }

        // *** Event Handlers ***

        private void PhoneApplicationFrame_RootVisualLoaded(PhoneApplicationFrame frame)
        {
            // Attach to the navigation events

            frame.Navigating += new NavigatingCancelEventHandler(NavigationService_Navigating);
            frame.Navigated += new NavigatedEventHandler(NavigationService_Navigated);

            // Set the current page

            CurrentPage = frame.Content as PhoneApplicationPage;

            // If the view model implements IPhoneLifetimeAware, then call Activated() or Launched()

            if (CurrentViewModel is IPhoneLifetimeAware)
            {
                if (isActivating)
                    ((IPhoneLifetimeAware)CurrentViewModel).Activated();
                else
                    ((IPhoneLifetimeAware)CurrentViewModel).Launched();
            }

            // Inform the initial view model of the navigation to it

            SignalNavigatedTo(CurrentViewModel, CurrentPage, new NavigationEventArgs(CurrentPage, frame.CurrentSource));
        }

        private void PhoneApplicationService_Activated(object sender, ActivatedEventArgs e)
        {
            // Set a flag to indicate that we are activating the application

            isActivating = true;
        }

        private void PhoneApplicationService_Closing(object sender, ClosingEventArgs e)
        {
            // If the view model implements IPhoneLifetimeAware, then call Closed()
            // NB: We use 'previousPage' as this event comes after the Navigated event

            if (PreviousViewModel is IPhoneLifetimeAware)
                ((IPhoneLifetimeAware)PreviousViewModel).Closed();
        }

        private void PhoneApplicationService_Deactivated(object sender, DeactivatedEventArgs e)
        {
            // If the view model implements IPhoneLifetimeAware, then call Deactivated()
            // NB: We use 'previousPage' as this event comes after the Navigated event

            if (PreviousViewModel is IPhoneLifetimeAware)
                ((IPhoneLifetimeAware)PreviousViewModel).Deactivated();

            // Shutdown any active services that implement IHasSessionState and store state

            foreach (KeyValuePair<Type, object> serviceEntry in serviceDictionary)
            {
                if (serviceEntry.Value is IHasSessionState)
                {
                    IHasSessionState phoneService = serviceEntry.Value as IHasSessionState;

                    object state = phoneService.SaveState();
                    string serviceStateKey = GetServiceStateKey(serviceEntry.Key);
                    PhoneApplicationService.Current.State[serviceStateKey] = state;
                }
            }
        }

        private void NavigationService_Navigating(object sender, NavigatingCancelEventArgs e)
        {
            // Try to inform the new view model of the navigation
            // NB: Only do this if the page navigating from is a real page (i.e. not "app://external/")

            if (CurrentPage != null)
                SignalNavigatingFrom(CurrentViewModel, CurrentPage, e);
        }

        private void NavigationService_Navigated(object sender, NavigationEventArgs e)
        {
            // Set the CurrentPage and keep a note of the previous page (for Closed/Deactivated messages)

            PreviousPage = CurrentPage;
            CurrentPage = e.Content as PhoneApplicationPage;

            // Try to inform the new view models of the navigation
            // NB: Only do this if the page navigated from/to is a real page (i.e. not "app://external/")

            if (PreviousPage != null)
                SignalNavigatedFrom(PreviousViewModel, PreviousPage, e);

            if (CurrentPage != null)
                SignalNavigatedTo(CurrentViewModel, CurrentPage, e);
        }

        // *** Event Methods ***

        private void OnViewModelInitialized(object viewModel)
        {
            if (ViewModelInitialized != null)
                ViewModelInitialized(this, new ViewModelEventArgs(viewModel));
        }
    }
}
